Skip to content

feat(embedded): add link delegation extension#247

Open
westeezy wants to merge 2 commits intoUniversal-Commerce-Protocol:mainfrom
westeezy:feat/ecp-link-delegation
Open

feat(embedded): add link delegation extension#247
westeezy wants to merge 2 commits intoUniversal-Commerce-Protocol:mainfrom
westeezy:feat/ecp-link-delegation

Conversation

@westeezy
Copy link
Contributor

@westeezy westeezy commented Mar 9, 2026

Description

Embedded Checkout can present links from the business (e.g., privacy policy, terms of service). By default, the checkout handles these internally. Link delegation lets the host claim control: when ec_delegate=link.open is set, the Embedded Checkout MUST send ec.link.open_request on buyer link activation, and the host MUST present the content or respond with a link_rejected error.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as expected, including removal of schema files
    or fields
    )
  • Documentation update

Is this a Breaking Change or Removal?

If you checked "Breaking change" above, or if you are removing any schema
files or fields:

  • I have added ! to my PR title (e.g., feat!: remove field).
  • I have added justification below.
## Breaking Changes / Removal Justification

(Please provide a detailed technical and strategic rationale here for why this
breaking change or removal is necessary.)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Embedded Checkout can present links from the business (e.g., privacy
policy, terms of service). By default, the checkout handles these
internally. Link delegation lets the host claim control: when
ec_delegate=link.open is set, the Embedded Checkout MUST send
ec.link.open_request on buyer link activation, and the host MUST
present the content or respond with a link_rejected error.
@westeezy westeezy requested review from a team as code owners March 9, 2026 21:01
@igrigorik igrigorik added the TC review Ready for TC review label Mar 10, 2026
@igrigorik igrigorik added this to the Working Draft milestone Mar 10, 2026
Copy link
Contributor

@igrigorik igrigorik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

- **MUST** present the buyer with visible feedback for every
`ec.link.open_request` — either the content itself (e.g., in a modal,
side panel, or new tab) or a notification that their link request was
rejected

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to mandate the Host to show an error to the Buyer for invalid links? I think that's more of an implementation choice. I'd suggest this

  • SHOULD validate the requested URL against host security policies (e.g., verifying origins).
  • MUST present the content to the buyer for every approved request (e.g., in a modal, new tab, or etc).
  • MUST respond with a JSON-RPC success (result) when the request was processed, or link_rejected if host policy prevented the navigation.
  • MAY notify the buyer if the request was rejected.

@mmohades
Copy link

Hey Westin, thank you so much for putting this together on such a short time! The design is very clean! As I was reading through your proposal, I realized we have two potential paths we can take here depending on how dynamic we want this delegation to be. I’d love to get your thoughts on which direction feels better to you:

A: ec.links.open_request

If we stick closely to the current proposal, we should probably rename the extension to ec.links to conceptually match the checkout.links array (similar to how ec.line_items.change maps to the line_items array).

However, if we tie the action directly to this specific checkout data namespace, it logically implies the merchant must declare all valid links in their UCP checkout object upfront. Here's an example request:

{
    "id": "...",
    "method": "ec.links.open_request",
    "params": {
        "type": "privacy_policy",
        "url": "https://merchant.com/privacy-policy"
    }
}
  • Pros: It's secure by default. By definition, the host only ever opens URLs that were pre-approved and vetted in previous steps.
  • Cons: It severely limits flexibility. The checkout wouldn't be able to open dynamic or ad-hoc links (e.g. a 3DS popup) without updating the checkout object first (which we also don't have a way to update in ECP, like ec.links.change)

B: (ec.navigation.open_request)

If we don't want merchants to have to pre-declare every single clickable link in advance, we could decouple this from the checkout.links data array entirely. We instead model this as a new, abstract routing extension. Here's an example request:

{
    "id": "....",
    "method": "ec.navigation.open_request",
    "params": {
        "url": "https://merchant.com/privacy-policy",
        "type": "privacy_policy"
    }
}
  • Pros: It gives the embedded checkout more flexibility to handle dynamic content, and avoids namespace or semantic confusion with the checkout.links data.
  • Cons: It pushes the security burden entirely onto the host since the URLs can't be validated in advance.

Thoughts

Both options are good to me. I'd like to hear your thoughts.

Presentation

On a completely different note, do we want to give the Business the option to choose how the link opens? For example, adding an optional presentation hint to the payload (like overlay vs new tab) so the checkout can suggest whether this should be a quick modal or a full-page redirect. We'd have to keep it as a suggestion though, since for example "new tab" doesn't mean anything to a native iOS host app. Or we could enforce it but expect the Host to reject if it doesn't support.

@jingyli
Copy link
Contributor

jingyli commented Mar 13, 2026

Overall the proposal makes sense to me! Just 2 fly-by generic thoughts/questions:

  1. This is the first time we are enabling a delegation pattern in ECP that anchors on a piece of data/concept of which platforms have no control over its entire value, unlike fulfillment & payment. It's also the only transport/instance where platforms can actually give some kind of feedback back to the business regarding this read-only value. It makes sense given the only action we are allowing now is open on links, but I wonder if this pattern is generally extensible to other similar fields in the future, if a use case arise. For example: host may want to claim control over error message display UI from Embedded Checkout, can a similar approach be applied?

  2. More of a small nit: We current have some language mentioning how the message name for the delegation should be structured (bullet #1 under "Delegation Flow") - ec.{capability}.{action}_request. I think what's slightly confusing to me is that link is a type vs. a capability like fulfillment or payment. Very small thing, but I'm wondering if we should update the language to be a bit clearer (i.e. ec.{capability/type}.{action}_request or delete this reference entirely)?

@gsmith85
Copy link

gsmith85 commented Mar 14, 2026

Following @mmohades' (#247 (comment)) feedback, I’m leaning toward Option B (ec.navigation.open_request) as the more robust path. It provides an opportunity to bridge links and the documented Navigation Constraint Permitted Exceptions.

As noted, Option A is problematic for dynamic redirects like 3DS, but also for other exceptions like payment provider redirects (PayPal/Klarna). These redirects often involve 'create Intent' calls for tokenized sessions generated on click, preventing the final URL from being whitelisted in advance.

Allowing the host to handle these scenarios addresses the nested Iframe problem on web and the session/bridge limitations of WebViews on mobile. Even with a correct merchant whitelist, browser security policies (X-Frame-Options/CSP) frequently block challenges when nested inside cross-origin 'middleman' iframes. Delegating to the host allows the challenge to be presented in a top-level context where those security headers can pass.

Even with Option B, using link delegation for the Navigation Exception cases will require separate consideration that need not block this PR. Specifically, it would require defining a generalized propagation pattern to return resulting tokens back to the iframe and evaluating the impact on PCI scope. Nonetheless, this affords a standardized escape hatch out and a functional channel back in.

"schema": {
"type": "object",
"description": "Acknowledgement that the host handled the link.",
"additionalProperties": false

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per #247 (comment), let's hold off on marking additionalProperties: false to maintain forward compatibility?

If we eventually want to use this pattern for Navigation Constraints/Permitted Exceptions, the host will need a way to pass data back to the iframe. Leaving the result object open allows for that future extensibility without breaking the core schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants